﻿using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.SqlCommand;
using NHibernate.Type;

namespace RG.Common.Provider
{
    public static class Criteria_Ext
    {
        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля field
        /// попадает во временной интервал r
        /// </summary>
        public static void AddDateRange(this DetachedCriteria q, string field, DateTime? minDate, DateTime? maxDate)
        {
            q.Add(RestrictionForDateRange(field, minDate, maxDate));
        }

        public static ICriterion RestrictionForDateRange(string field, DateTime? minDate, DateTime? maxDate)
        {
            if (minDate.HasValue && maxDate.HasValue)
               return Restrictions.Between(field, minDate, maxDate);
            if (minDate.HasValue)
                return Restrictions.Ge(field, minDate);
            if (maxDate.HasValue)
                return Restrictions.Le(field, maxDate);
            return null;
        }


        public static void AddDate(this DetachedCriteria q, string field, DateTime? r)
        {
            if (r.HasValue)
            {
                q.Add(Restrictions.Eq(field, r.Value));
            }
        }

        /// <summary>
        /// Создаёт критерий 'coalesce(field, field) between minValue and maxValue'
        /// </summary>
        public static void AddNotIndexedBetween(this DetachedCriteria q, string field, object minValue, object maxValue)
        {
            var args = new IProjection[] { Projections.Property(field), Projections.Property(field) };
            q.Add(Restrictions.Between(Projections.SqlFunction("coalesce", NHibernateUtil.DateTime, args), minValue, maxValue));
        }

        public static void AddDateRange(this DetachedCriteria q, string field, DatesRange r)
        {
            if (r != null)
            {
                q.AddDateRange(field, r.minDate, r.maxDate);
            }

        }

        public static void AddRange<T>(this DetachedCriteria q, string field, Interval<T> interval)
        {
            if (interval != null)
            {
                q.Add(Restrictions.Between(field, interval.From, interval.To));
            }
        }


        public static void AddDateRanges(this DetachedCriteria q, string field, List<DatesRange> list)
        {
            if (list != null && list.Any())
            {
                ICriterion criterion = null;

                foreach (var datesRange in list)
                {
                    var cur = RestrictionForDateRange(field, datesRange.minDate, datesRange.maxDate);
                    criterion = criterion == null ? cur : Restrictions.Or(criterion,cur);
                }
                q.Add(criterion);
            }
        }

        /// <summary>
        /// Если в базе поле TimeStamp, то используем этот метод
        /// </summary>
        /// <param name="q"></param>
        /// <param name="field"></param>
        /// <param name="r"></param>
        public static void AddTimeStampRange(this DetachedCriteria q, string field, DatesRange r)
        {
            if (r == null) return;
            if (r.minDate.HasValue && r.maxDate.HasValue)
            {
                q.Add(Restrictions.And(Restrictions.Ge(field, r.minDate.Value),
                                       Restrictions.Lt(field, r.maxDate.Value.AddDays(1).Date)));
            }
            else if (r.minDate.HasValue)
                q.Add(Restrictions.Ge(field, r.minDate.Value.Date));
            else if (r.maxDate.HasValue)
                q.Add(Restrictions.Lt(field, r.maxDate.Value.AddDays(1).Date));
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля field
        /// начинается с value 
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">значение</param>
        public static void AddStartingWith(this DetachedCriteria q, string field, string value)
        {
            if (string.IsNullOrEmpty(value)) return;
            q.Add(new StartingWithExpression(field, value.Trim()));
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля field
        /// совпадает со значением value и подходит под условия fromStart и caseSensitive
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">значение</param>
        /// <param name="fromStart">Проверять с начала строки</param>
        /// <param name="caseSensitive">Чувствительность к регистру</param>
        public static void AddString(this DetachedCriteria q, string field, string value, bool fromStart,
                                  bool caseSensitive)
        {
            if (string.IsNullOrEmpty(value)) return;

            // Оптимизация под FIREBIRD:
            // Индекс по текстовому полю не применяется в выражении "NUM_DOC like ?param" (хотя применяется в "NUM_DOC like '170-1234%'")
            if (fromStart && caseSensitive)
            {
                var sqlString =
                    (new SqlStringBuilder())
                        .Add("{alias}." + field + " starting with ")
                        .AddParameter()
                        .ToSqlString();
                q.Add(new SQLCriterion(sqlString,
                    new object[] { value },
                    new IType[] { NHibernate.NHibernateUtil.String }));
                return;
            }

            var matchMode = fromStart ? MatchMode.Start : MatchMode.Anywhere;

            q.Add(caseSensitive
                      ? Restrictions.Like(field, value.Trim(), matchMode)
                      : Restrictions.InsensitiveLike(field, value.Trim(), matchMode));
        }

        public static void AddNullableInt(this DetachedCriteria q, string field, int? value)
        {
            q.Add(value.HasValue ? Restrictions.Eq(field, value.Value) : Restrictions.IsNull(field));
        }

        public static void AddInt(this DetachedCriteria q, string field, int? value)
        {
            if (value.HasValue)
                q.Add(Restrictions.Eq(field, value.Value));
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля
        /// field совпадает хотя бы с одним значением из массива value
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">массив значений</param>
        public static void AddInts<T>(this DetachedCriteria q, string field, ICollection<T> value)
        {
            if (value != null && value.Any())
                q.Add(Restrictions.InG(field, value));
        }

        /// <summary>
        /// Аналогично AddItens, но при пустом списке в запросе получается пустой результат
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">массив значений</param>
        public static void AddIntList<T>(this DetachedCriteria q, string field, ICollection<T> value)
        {
            if (value == null) return;
            q.Add(value.Any() ? Restrictions.InG(field, value) : Restrictions.IsNull(field));
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля
        /// field совпадает хотя бы с одним значением из массива value
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">массив значений</param>
        public static void AddEnum<T>(this DetachedCriteria q, string field, ICollection<T> value)
        {
            if (value != null && value.Any())
                q.Add(Restrictions.InG(field, value));
        }


        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля 
        /// С_Tree, принадлежащего объекту в поле field, совпадает хотя бы с одним значением из 
        /// массива value
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">имя поля</param>
        /// <param name="value">массив значений</param>
        /// <param name="withChildren">массив значений</param>
        public static void AddTreeIds(this DetachedCriteria q, string field, IEnumerable<int> value, bool withChildren)
        {
            if ((value == null) || (value.Count() <= 0)) return;
            var subQuery = q.CreateCriteria(field);
            if (withChildren)
                subQuery = subQuery.CreateCriteria("Parents");
            subQuery.Add(Restrictions.In("C_Tree", value.ToArray()));
        }


        public static void AddShopIds(this DetachedCriteria q, string shopFieldName, IEnumerable<int> value)
        {
            if (value == null || !value.Any()) return;
            var subQuery = q.CreateCriteria("Shop");
            subQuery.AddInts("C_Shop", value.ToArray());
        }

        public static void AddShopRegions(this DetachedCriteria q, string shopFieldName, IEnumerable<int> value)
        {
            if (value == null || !value.Any()) return;
            var subQuery = q.CreateCriteria("Shop");
            subQuery.AddInts("Region", value.ToArray());
        }

        public static void AddShopDirectors(this DetachedCriteria q, string shopFieldName, IEnumerable<int> value)
        {
            if (value == null || !value.Any()) return;
            var subQuery = q.CreateCriteria("Shop");
            subQuery.AddInts("Director", value.ToArray());
        }

       
        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых объект S_Tree в свойстве 
        /// field имеет родителя c идентификатором из коллекции value
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">поле S_Tree</param>
        /// <param name="value">значение идентификаторов</param>
        public static void AddParentTreeIds(this DetachedCriteria q, string field, IEnumerable<int> value)
        {
            if ((value == null) || (value.Count() <= 0)) return;
            var subQuery = q.CreateCriteria(field);
            subQuery.Add(Restrictions.In("C_Parent_Tree", value.ToArray()));
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля 
        /// field не null и если condition == true;
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">поле которое нужно проверить на null</param>
        /// <param name="condition">условие при котором нужно проверить на null</param>
       
        public static void AddIsNotNull(this DetachedCriteria q, string field, bool condition)
        {
            if (condition)
                q.Add(Restrictions.IsNotNull(field));
        }

        public static void AddIsNotNullOrNull(this DetachedCriteria q, string field, bool? condition)
        {
            if (condition.HasValue)
            {
                if (condition.Value)
                    q.Add(Restrictions.IsNotNull(field));
                else
                    q.Add(Restrictions.IsNull(field));
            }
        }

        public static void AddIsNullOrNotNull(this DetachedCriteria q, string field, bool? condition)
        {
            if (condition.HasValue)
            {
                if (condition.Value)
                    q.Add(Restrictions.IsNull(field));
                else
                    q.Add(Restrictions.IsNotNull(field));
            }
        }

        /// <summary>
        /// Добавляет условие, при котором в выборку попадают все объекты, у которых значение поля 
        /// field не null и если condition == true;
        /// </summary>
        /// <param name="q">критерий</param>
        /// <param name="field">поле которое нужно проверить на null</param>
        /// <param name="condition">условие при котором нужно проверить на null</param>

        public static void AddIsNullOrZero(this DetachedCriteria q, string field, bool? condition)
        {
            if(condition.HasValue)
            {
                if (condition.Value)
                    q.Add(Restrictions.And(Restrictions.IsNotNull(field), Restrictions.Not(Restrictions.Eq(field,0))));
                else
                    q.Add(Restrictions.Or(Restrictions.IsNull(field), Restrictions.Eq(field, 0)));
            }
        }

        /// <summary>
        /// Проверяет, что после наложения битовой маски 'mask' поле 'field' равно 'value'
        /// </summary>
        public static void AddMask(this DetachedCriteria q, string field, int mask, int value)
        {
            if (mask == 0 && value == 0) return; // always true

            var sqlString =
                (new SqlStringBuilder())
                    .Add("bin_and({alias}." + field + ",")
                    .AddParameter()
                    .Add(")=")
                    .AddParameter()
                    .ToSqlString();
            q.Add(new SQLCriterion(sqlString,
                new object[] { mask, value },
                new IType[] { NHibernate.NHibernateUtil.UInt32, NHibernate.NHibernateUtil.UInt32 }));
        }
    }
}